# -- coding: utf-8 --
# @time :
# @author : 周梦泽
# @file : .py
# @software: pycharm
import threading
import time
import traceback

from dataclasses import dataclass
import random
from typing import List, Union, Optional, Tuple, Callable

import pywinauto
from DrissionPage import WebPage, ChromiumOptions
from DrissionPage.chromium_element import ChromiumElement
from DrissionPage.chromium_tab import ChromiumTab, WebPageTab
from DrissionPage.commons.constants import NoneElement
from DrissionPage.tools import Listener, RequestMan
from FlowViewer.request_man import RequestData
# common/utils/AppToForeground.py
from common.utils.AppToForeground import bring_app_to_foreground
from common.logger.log import log_
from common.utils import mouseUtil
from config import data as config

# 操作锁
action_lock = threading.Lock()


def test_page(port=config.get('browser.port'), mode='d') -> WebPage:

    log_.info('接管浏览器')
    do = ChromiumOptions().set_paths(local_port=port).set_argument(
        '--disable-cache').set_argument(
        '--disable-password-generation').set_argument(
        '--disable-ios-password-suggestions')
    page = WebPage(mode, driver_or_options=do)
    return page


def create_page(url: str = None, port=config.get('browser.port'), mode='d', del_other_page=True,
                is_refresh=True) -> WebPage:
    """
    启动浏览器 创建网页对象page
    :param is_refresh: 创建页面时是否刷新页面
    :param del_other_page: 启动时创建页面时是否删除其他标签页
    :param mode: 启动模式 d:driver 有界面, s:service 无界面
    :param port: 端口号
    :param url: 需要打开的网页地址
    :return:page, status page:网页对象 status:状态信息
    :rtype: page
    """

    log_.info('正在启动浏览器')
    do = ChromiumOptions().set_paths(local_port=port).set_argument(
        '--disable-cache').set_argument(
        '--disable-password-generation').set_argument(
        '--disable-ios-password-suggestions')
    # 去除通知
    do.set_pref(arg="credentials_enable_service", value='0')
    do.set_pref(arg="profile.password_manager_enabled", value='0')
    do.set_pref(arg='profile.default_content_settings.popups', value='0')  # 去除弹窗
    # 去除密码管理

    page = WebPage(mode, driver_or_options=do)
    if is_refresh and page.url != "chrome://newtab/":
        page.refresh()
    page.handle_alert(accept=True)
    page.set.window.maximized()
    bring_app_to_foreground('Chrome')
    log_.info('浏览器启动成功')
    if del_other_page:
        page.close_other_tabs()
    if url:
        page.get(url)
        page.handle_alert(accept=True)
    #   当跳转网页时有可能弹出是否离开此网站的提示框，这里直接点击确定
    return page


def response_listener(targets, count: int = 1, timeout: int = 10, port=config.get('browser.port')):
    """
    响应监听器
    :param count: 监听响应数量 默认 1
    :param timeout: 监听超时时间 默认 10
    :param port: 监听 游览器端口 默认 配置文件端口
    :param targets: 监听 请求 url 支持单个 字符串 列表
    :return: 返回一个 字典 结果为 target 于 其对应的body数组
    :rtype: dict
    """
    listener = Listener(port)
    listener.set_targets(targets)
    listener.listen(count=count, timeout=timeout)
    result = {}
    for item in listener.steps():
        content_type = item.headers.get("content-type")
        status = item.response.get('status')
        if item.response.get('status') != 200:
            log_.warning(f"监听目标请求失败 target : {item.target} status : {status}")
            continue
        if content_type.lower() == "application/json;charset=UTF-8".lower():
            if result.get(item.target) is None:
                result[item.target] = [item.body]
            else:
                result.get(item.target).append(dict(item.body))
        else:
            log_.warning(f"监听器遇到了暂时不支持解析的响应正文 target: {item.target}")
        log_.info("===========================监听响应内容================================")
        log_.info(
            f"target: {item.target} \n"
            + f"headers: {item.headers} \n"
            + f"url: {item.url} \n"
            + f"status: {item.response.get('status')} \n"
            + f"content-type: {content_type} \n"
            + f"body: {item.body}")
        log_.info("=================================================================")
    return result


def request_listener(targets, count: int = 1, timeout: int = 10, port=config.get('browser.port')) -> List[RequestData]:
    """
    请求监听器
    :param count: 监听请求数量 默认 1
    :param timeout: 监听超时时间 默认 10
    :param port: 监听 游览器端口 默认 配置文件端口
    :param targets: 监听 请求 url
    :return: 返回一个 RequestData 列表
    :rtype: List[RequestData]
    """
    listener = RequestMan(port)
    listener.set_targets(targets)
    listener.listen(count=count, timeout=timeout)
    return [item for item in listener.steps()]


def wait_ele_disappear(page: WebPage, ele_find_str: str, timeout: int):
    """
    等待元素消失(定位不到)
    :param page:页面对象
    :param ele_find_str: 搜索元素的字符串
    :param timeout: 超时时间
    :return: 在超时时间内消失 为 true 否为 false
    :rtype: bool
    """
    for _ in range(timeout):
        ele = page(ele_find_str, timeout=1)
        if not ele:
            return True
        elif not ele.states.is_displayed:
            return True
        else:
            time.sleep(1)
    return False


def wait_ele_text_range(ele: ChromiumElement, low_text: str, timeout: int = 10, throw_error=False):
    """
    等待元素文本发生该表
    :param ele: 元素
    :param low_text: 初始文本
    :param timeout: 超时时间
    :param throw_error: 失败是否直接抛出异常
    :return: 返回一个元组 1为 成败 2为 成功 新文本  失败旧文本
    :rtype: tuple[bool, str]
    """
    for _ in range(timeout):
        time.sleep(1)
        if ele.text != low_text:
            return True, ele.text
    if throw_error:
        raise RuntimeError(f"等待元素文本更改失败 元素: {ele} 旧文本： {low_text}")
    return False, ele.text


def get_first_visible_ele(p: WebPage, select_str: str) -> Optional[ChromiumElement]:
    """
    传入一个多个元素匹配字符串返回第一个可见的元素
    :param p: 网页对象
    :param select_str: 元素选择器
    :return: 第一个可见元素对象 , 没有获取到则返回 None
    """
    return next((e for e in p.eles(select_str, timeout=0) if e.states.is_displayed), None)


@dataclass
class Action:
    """
    动作对象
    Attributes:
        ele: 元素定位符,元素对象,返回一个网页元素的方法
        msg: 错误信息
        before_delay: 点击之前延迟
        after_delay: 点击之后延迟
        tab_id: 当前元素所在标签页id
    """
    ele: Union[str, ChromiumElement, NoneElement, Callable]
    msg: Optional[str] = None
    before_delay: [int, Tuple[int, int]] = 0
    after_delay: [int, Tuple[int, int]] = 0
    tab_id: Optional[str] = None

    def init_ele(self, page: [WebPage, ChromiumTab]):
        """
        初始化元素
        :param page: 网页对象或标签页对象
        :return:
        """
        if not isinstance(self.ele, ChromiumElement) and isinstance(self.ele, Callable):
            print(self.ele, type(self.ele))
            self.ele = self.ele()
        if isinstance(self.ele, str):
            self.ele = page(self.ele)
        if self.ele and self.tab_id is None:
            self.tab_id = self.ele.page.tab_id

    def active_ele_to_mock_action_status(self):
        """
        激活元素至可执行模拟动作状态
        :return:
        """
        # 将网页窗口至于前台
        set_page_active(self.ele.page)
        if isinstance(self.ele.page, WebPageTab):
            self.ele.page.page.to_main_tab()
        elif isinstance(self.ele.page, ChromiumTab):
            self.ele.page.page.to_main_tab()
        else:
            self.ele.page.to_main_tab()
        # 页面滚动到元素可见
        self.ele.page.scroll.to_see(self.ele)


@dataclass
class Tap(Action):
    """
    Tap 方法参数
    左键点击封装
    :param is_random: 是否随机点击
    :param by_js : 是否使用 js点击(使用js 则随机失效)
    """
    is_random: bool = True
    by_js: bool = False

    def __call__(self, page: [WebPage, ChromiumTab] = None):
        try:
            action_lock.acquire()
            self.init_ele(page)
            if self.ele:
                random_delay(self.before_delay)
            try:
                if self.by_js:
                    self.ele.click(by_js=self.by_js)
                elif self.is_random:
                    size_box = self.ele.size
                    x = random.randint(1, size_box[0] - 1)
                    y = random.randint(1, size_box[1] - 1)
                    self.ele.click.at(x, y)
                else:
                    self.ele.click()
            except Exception as e:
                if self.msg is not None:
                    raise RuntimeError(self.msg)
                raise e
            random_delay(self.after_delay)
        finally:
            action_lock.release()


@dataclass
class MockTap(Action):
    """
    拟人点击操作对象
    Attributes:
        scope_random: 是否范围随机点击
        offset: 偏移坐标点击 (x,y)
        reduce_scope: 缩小随机范围 (用于解决随机范围过大点击不到元素问题)(x, y)
        is_ele_unavailable_err: 点击元素未启动时报错
    """
    scope_random: bool = True
    offset: Tuple[int, int] = (0, 0)
    reduce_scope: Tuple[int, int] = (1, 1)
    is_ele_unavailable_err: bool = True

    def __call__(self, page: [WebPage, ChromiumTab] = None):
        try:
            action_lock.acquire()
            self.init_ele(page)
            if not self.ele:
                raise RuntimeError(f"没有找到该元素 : {self}")
            if self.is_ele_unavailable_err and not self.ele.states.is_enabled:
                raise RuntimeError("元素未启用不可点击")
            random_delay(self.before_delay)
            self.active_ele_to_mock_action_status()
            if self.scope_random:
                x, y = self.ele.locations.screen_location
                mouseUtil.scope_random_tap((x, y), self.ele.size, reduce_scope=self.reduce_scope)
            else:
                x, y = self.ele.locations.screen_midpoint
                mouseUtil.tap(x + self.offset[0], y + self.offset[1])
            random_delay(self.after_delay)
        finally:
            action_lock.release()


@dataclass
class MockMove(Action):
    """
        拟人拖动鼠标到指定元素
        scope_random: 元素范围随机点
        reduce_scope: 缩小随机范围
        offset: 移动偏移坐标(x,y)
    """
    scope_random: bool = True
    reduce_scope: Tuple[int, int] = (1, 1)
    offset: Tuple[int, int] = (0, 0)

    def __call__(self, page: WebPage = None):
        try:
            action_lock.acquire()
            self.init_ele(page)
            random_delay(self.before_delay)
            self.active_ele_to_mock_action_status()
            if self.scope_random:
                point = get_ele_random_point(self.ele, self.reduce_scope)
                mouseUtil.move(point[0], point[1])
            else:
                x, y = self.ele.locations.screen_location
                mouseUtil.move(x + self.offset[0], y + self.offset[1])
            random_delay(self.after_delay)
        finally:
            action_lock.release()


@dataclass
class Input(Action):
    """
    输入操作对象
    Attributes:
        vals: 一串字符串或者是一个组合键元组
        before_clear: 输入之前清空
    """
    vals: [str, tuple] = None
    before_clear: bool = True

    def __call__(self, page: WebPage = None):
        try:
            action_lock.acquire()
            self.init_ele(page)
            random_delay(self.before_delay)
            self.ele.input(self.vals, clear=self.before_clear)
            random_delay(self.after_delay)
        finally:
            action_lock.release()


@dataclass
class ActionChain:
    """
    动作链对象
    Attributes:
        page: 网页对象
    """
    page: WebPage

    def __call__(self, action_list: list):
        """
        :param action_list: 操作参数对象列表 如 Click0,Input0
        :return: 链本身
        """
        for index, action in enumerate(action_list):
            try:
                if isinstance(action, Tap):
                    self.tap(action)
                elif isinstance(action, Input):
                    self.input(action)
                elif isinstance(action, MockTap):
                    self.mock_tap(action)
                elif isinstance(action, MockMove):
                    self.mock_move(action)
                else:
                    raise RuntimeError(f"暂不支持的操作 {action}")
            except Exception as e:
                log_.error(f"ActionChain call failed index : {index}")
                raise e
        return self

    def tap(self, obj: Tap):
        """
        根据 Tap 对象进行点击
        :param obj:
        :return: 链本身
        """
        try:
            obj(self.page)
        except Exception as e:
            log_.error(f"ActionChain -> tap 点击失败  : {obj}")
            raise e
        return self

    def mock_tap(self, obj: MockTap):
        """
        调用拟人点击
        :param obj: 模拟点击对象
        :return: 链本身
        """
        try:
            obj(self.page)
        except Exception as e:
            log_.error(f"ActionChain -> mock_tap 进行点击失败  : {obj}")
            log_.error(traceback.format_exc())
            raise e
        return self

    def mock_move(self, obj: MockMove):
        """
        调用模拟鼠标移动
        :param obj: 模拟启动对象
        :return: 链本身
        """
        try:
            obj(self.page)
        except Exception as e:
            log_.error(f"ActionChain -> mock_move 进行移动失败 : {obj}")
            raise e
        return self

    def input(self, obj: Input):
        """
        根据输入对象进行输入
        :param obj: 输入对象
        :return: 链本身
        """
        try:
            obj(self.page)
        except Exception as e:
            log_.error(f"根据对象输入失败 obj : {obj}")
            raise e
        return self


def random_delay(scope: [int, Tuple[int, int]]):
    """
    随机延迟
    :param scope: 延迟范围 例如[(0,1)]
    :return:
    """
    if isinstance(scope, int):
        time.sleep(scope)
    else:
        time.sleep(random.uniform(scope[0], scope[1]))


def get_ele_random_point(ele: ChromiumElement, reduce_scope: Tuple[int, int] = (1, 1)):
    """
    获取元素范围内的任一一点
    :param ele: 元素
    :param reduce_scope: 所见随机范围
    :return: x , y
    """
    # 获取左上角坐标
    left_top_point = ele.locations.screen_location
    # 获取元素大小
    size = ele.size
    x = random.randint(left_top_point[0] + reduce_scope[0], left_top_point[0] + size[0] - reduce_scope[0])
    y = random.randint(left_top_point[1] + reduce_scope[1], left_top_point[1] + size[1] - reduce_scope[1])
    return x, y


def set_page_active(page: [WebPage, ChromiumTab], is_window_max: bool = True):
    """
    激活网页页面的win窗口至前台
    :param page: 网页对象
    :param is_window_max: 是否设置窗口最大化
    :return:
    """
    if isinstance(page, ChromiumTab):
        page = page.page
    hwnd = pywinauto.findwindows.find_windows(process=page.process_id)
    if len(hwnd) > 0:
        win = pywinauto.Application().connect(handle=hwnd[len(hwnd) - 1]).window(handle=hwnd[len(hwnd) - 1])
        if not win.is_active():
            win.set_focus()
        if is_window_max:
            page.set.window.maximized()
    else:
        raise RuntimeError("获取不到当前页面的窗口句柄")


if __name__ == '__main__':
    url_ = 'https://www.baidu.com'
    p = create_page()
